/*
 * Copyright (c) 2014, Hsu Shih-Chia (schsu01@gmail.com)
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qood;

import java.io.File;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.RandomAccessFile;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;

/**
 * QBuilder is a tool for creating QModel manually.
 *
 * @author SCHsu01
 * @version 1.0
 */
public interface QBuilder {
  /**
   * well done. Return the result.
   *
   * @return created QModel
   */
  QModel toModel();

  /**
   * Get how many rows in this QBuilder
   *
   * @return row count
   */
  int getRowCount();

  /**
   * Provide a tool for creating a multi-column QModel by adding each data row manually.
   * <p>The following example illustrates how to create a QModel contains 3 columns:
   * <pre><code>
   * final QModel fruits = QNew.builderHeap(2, "NAME", "PRICE", "AMOUNT")//2 rows, with 3 columns
   *   .setTypes(String.class, Double.class, Integer.class)//set column types before add rows
   *   .add("Banana", 2.5, 7)
   *   .add("Papaya", 3.1, 2)
   *   .toModel().debug(-1);
   * </code></pre>
   */
  interface Cols extends QBuilder {
    /**
     * Set column types before add rows
     *
     * @param types data type of each columns
     * @return a reference to this Object
     */
    Cols setTypes(Class... types);

    /**
     * Add a new row manually
     *
     * @param values content of each columns
     * @return a reference to this Object
     */
    Cols add(Object... values);

    /**
     * Enable type detection for given column (its type must be String, BigDecimal or Object)
     *
     * @param names columns to detect
     * @return a reference to this Object
     */
    Cols detectType(String... names);
  }

  /**
   * Provide a tool for creating a 2-column QModel by adding key~value pairs or java.util.Map.
   * <p>The following example illustrates how to create a QModel by {@link System#getenv()}:
   * <pre><code>
   * final QModel env = QNew.builderMap(String.class, String.class)
   *   .addAll(System.getenv())
   *   .toModel().debug(-1);
   * </code></pre>
   *
   * @param <K> type of key column
   * @param <V> type of value column
   */
  interface KeyValue<K extends Comparable, V> extends QBuilder {
    /**
     * Add a new row manually
     *
     * @param key   content of key
     * @param value content of value
     * @return a reference to this Object
     */
    KeyValue<K, V> add(K key, V value);

    /**
     * Add key~value rows by Map directly
     *
     * @param data predefined data
     * @return a reference to this Object
     */
    KeyValue<K, V> addAll(Map<K, V> data);

    /**
     * remove a row by specified key
     *
     * @param key content of key to be removed
     * @return a reference to this Object
     */
    KeyValue<K, V> remove(K key);

    /**
     * well done. Return the result with customized column name.
     *
     * @param keyCol   name of key column, default is "KEY"
     * @param valueCol name of value column, default is "VALUE"
     * @return created QModel
     */
    QModel toModel(String keyCol, String valueCol);
  }

  /**
   * Provide a tool to combine 2 or more QModels as a new bigger QModel (which contains total amount of rows).
   * <p>The following example illustrates how to create a combined QModel:
   * <pre><code>
   * final QModel unionB = QNew.builderUnion(null, "B")
   *   .add(QNew.builderHeap(1, "A", "B").setTypes(String.class, Integer.class).add("3", 4).toModel())
   *   .add(QNew.builderHeap(1, "B", "C").setTypes(Integer.class, Double.class).add(4, 5.0).toModel())
   *   .toModel().debug(-1);
   * </code></pre>
   */
  interface Union extends QBuilder {
    /**
     * Add rows by a QModel
     *
     * @param model data for combination
     * @return a reference to this Object
     */
    Union add(QModel model);

    /**
     * Add rows by QModels
     *
     * @param models data for combination
     * @return a reference to this Object
     */
    Union add(QModel... models);
  }

  /**
   * Provide a tool for creating a QModel by Reflection with defined fields or methods.
   * <p>The following example illustrates how to create a QModel of String:
   * <pre><code>
   * final QModel compiledString = QNew.builderPOJO(String.class)
   *   .method("TEXT", "toString")
   *   .field("CHAR_REF", "value")//indicated Object reference
   *   .method("MODIFY", "concat", new Class[]{String.class}, "!")
   *   .add("qood", "qo" + "od", "qo".concat("od"))
   *   .toModel().debug(-1);
   * </code></pre>
   *
   * @param <T> type of POJO which contains fields or methods you want
   */
  interface POJO<T> extends QBuilder {
    /**
     * Add a new column by simple method without arguments
     *
     * @param colName          new column name
     * @param getterMethodName method name like "toString"
     * @return a reference to this Object
     */
    POJO<T> method(String colName, String getterMethodName);

    /**
     * Add a new column by method with arguments
     *
     * @param colName          new column name
     * @param getterMethodName method name like "equals"
     * @param argTypes         type of argument list, Note: int.class is different from Integer.class
     * @param args             given arguments
     * @return a reference to this Object
     */
    POJO<T> method(String colName, String getterMethodName, Class[] argTypes, Object... args);

    /**
     * Add a new column by specified field
     *
     * @param colName   new column name
     * @param fieldName field name
     * @return a reference to this Object
     */
    POJO<T> field(String colName, String fieldName);

    /**
     * Add a new row
     *
     * @param value POJO
     * @return a reference to this Object
     */
    POJO<T> add(T value);

    /**
     * Add new rows
     *
     * @param values POJOS
     * @return a reference to this Object
     */
    POJO<T> add(T... values);
  }
}

interface Converter<F, T> {
  T to(F value);

  Class<T> toType();
}

abstract class Builder implements QBuilder.Cols {
  final int cols;
  final String[] names;
  Class[] types;
  private transient Model model;

  Builder(String... names) {
    if (new HashSet<String>(Arrays.asList(names)).size() < (this.cols = names.length))
      throw new IllegalArgumentException("Duplicated names: " + StringUtil.join(names, ", "));
    this.names = names.clone();
  }

  public final QModel toModel() {//once created, can't create again
    if (null == model) model = prepareModel();
    return model;
  }

  final void checkLength(int n) {
    if (n != cols) throw new IllegalArgumentException("input length(=" + n + ") should be " + cols);
  }

  @SuppressWarnings("unchecked")
  final void _setTypes(Class... type) {
    final int n = type.length;
    checkLength(n);
    if (null != types) for (int i = 0; i < n; i++)//check types
      if (!type[i].isAssignableFrom(types[i])) throw new ClassCastException(
        names[i] + " type changed " + types[i] + " to " + type[i]);
    types = type;
  }

  final void _add(Object[] values) {
    checkLength(values.length);
    addNoCheck(values);
  }

  final void _addAll(Col[] cols, int rowCnt) {
    if (rowCnt <= 0) return;
    final int col = cols.length;
    checkLength(col);
    final Object[] values = new Object[col];//reuse
    for (int row = 0; row < rowCnt; row++) addNoCheck(rowsForAdd(Output.getValues(row, col, cols, values)));
  }

  Object[] rowsForAdd(Object[] values) {
    return values;
  }

  abstract void addNoCheck(Object[] values);

  abstract Model prepareModel();
}

final class BuilderRow extends Builder {
  private final Collection<Object[]> rows;

  BuilderRow(int rowsMax, boolean distinct, String... names) {
    super(names);
    this.rows = distinct ? CollFactory.ARRAY.getSet(true, rowsMax) : new ArrayList<Object[]>(rowsMax);
  }

  public int getRowCount() {
    return rows.size();
  }

  public BuilderRow setTypes(Class... types) {
    _setTypes(types);
    return this;
  }

  public BuilderRow setTypes(Col... cols) {
    _setTypes(Col.typeOf(cols));
    return this;
  }

  public BuilderRow add(Object... values) {
    _add(values);
    return this;
  }

  public BuilderRow detectType(String... names) {
    throw new UnsupportedOperationException("Supported by Column-Based Builder only!");
  }

  public BuilderRow addAll(Col[] cols, int rowCnt) {
    _addAll(cols, rowCnt);
    return this;
  }

  @Override
  Object[] rowsForAdd(Object[] values) {
    return values.clone();
  }

  void addNoCheck(Object[] values) {
    rows.add(values);
  }

  Model prepareModel() {
    return new ModelArray(names, types, rows.toArray(new Object[rows.size()][]));
  }
}

abstract class BuilderCol extends Builder {
  final int bufSize;
  private final Object[][] buffer;
  private final Set<String> detect = new HashSet<String>();
  private int rowCnt, buffered, saved;
  private transient int[] convId;
  private transient Convert[] convert;

  BuilderCol(int bufSize, String... names) {
    super(names);
    this.bufSize = bufSize;
    buffer = new Object[cols][];
    resetBuffer();
    rowCnt = buffered = saved = 0;//reset all counter
  }

  static BuilderCol getInstance(String pathQFS, String... names) {
    return null == pathQFS ? new BuilderHeap(QNew.CACHE_SIZE, names) : new BuilderQFS(pathQFS, names);
  }

  public final int getRowCount() {
    return rowCnt;
  }

  public final BuilderCol add(Object... values) {
    _add(values);
    return this;
  }

  public final BuilderCol detectType(String... names) {
    detect.addAll(Arrays.asList(names));
    return this;
  }

  public final BuilderCol setTypes(Class... types) {
    _setTypes(types);
    return this;
  }

  final BuilderCol setTypes(Col... cols) {
    _setTypes(Col.typeOf(cols));
    return this;
  }

  void beforeFirstSaveBuffer() {
    if (detect.isEmpty())//check types
      if (null == types) throw new IllegalArgumentException("please setTypes(...) first");
      else return;
    final int n = cols;
    if (null == types) types = new Class[n];
    final Convert[] conv = new Convert[n];
    final IntList ids = IntList.create(n);
    for (int i = 0; i < n; i++)
      if (detect.contains(names[i]) && null != (conv[i] = Convert.detect(types[i], buffer[i]))) ids.add(i);
    if (ids.isEmpty()) return;//stop
    convert = conv;
    final Cols builder = QNew.builder(null, "ColName", "Before", "After")
      .setTypes(String.class, String.class, String.class);
    for (final int i : (convId = ids.toArray())) {
      final Class b = types[i], a = types[i] = convert[i].toType();//detected new type
      builder.add(names[i], null == b ? null : b.getSimpleName(), a.getSimpleName());
    }
    System.out.println("Type detection result for " + detect + " :");
    builder.toModel().debug(-1);
  }

  private Object[][] getBuffer(int bufSize) {
    if (0 == saved++) beforeFirstSaveBuffer();//first time save
    if (null != convId) for (final int i : convId) convert[i].modify(buffer[i], bufSize);//modify
    return buffer;
  }

  final void addNoCheck(Object[] values) {
    if (buffered == bufSize) {//buffer full
      saveBuffer(getBuffer(bufSize), bufSize);
      buffered = 0;//refresh index
    }
    for (int i = 0, n = cols, j = buffered++; i < n; i++) buffer[i][j] = values[i];
    rowCnt++;
  }

  final BuilderCol addAll(Col[] cols, int rowCnt) {
    _addAll(cols, rowCnt);
    return this;
  }

  final void resetBuffer() {
    for (int i = 0, n = cols, b = bufSize; i < n; i++) buffer[i] = new Object[b];//for each col
  }

  final Model prepareModel() {
    if (rowCnt == 0) beforeFirstSaveBuffer();
    else if (buffered > 0) saveBuffer(getBuffer(buffered), buffered);//handle remain rows
    return createModel(rowCnt);
  }

  abstract void saveBuffer(Object[][] buffer, int buffered);

  abstract Model createModel(int totalRows);
}

final class BuilderHeap extends BuilderCol {
  private final List<Object[][]> full = new ArrayList<Object[][]>();

  BuilderHeap(int bufSize, String... names) {
    super(bufSize, names);
  }

  void saveBuffer(Object[][] buffer, int buffered) {
    full.add(buffer.clone());
    resetBuffer();
  }

  @SuppressWarnings({"unchecked"})
  Model createModel(int totalRows) {
    final Col[] col = new Col[cols];
    if (totalRows == bufSize) {//gave exactly rowsMax
      final Object[][] buf = full.get(0);//use first buffer directly
      for (int i = 0; i < cols; i++) col[i] = Col.getValues(types[i], buf[i]);
    } else {//copyp as big array
      final Object[][] row = new Object[cols][];
      int i = 0, pos = 0;
      try {
        for (i = 0; i < cols; i++)
          row[i] = null == types[i] ? new Object[totalRows] :
            (Object[]) Array.newInstance(types[i], totalRows);//create buffer first
        for (final Object[][] buffer : full) {//copy content to buffer
          final int next = pos + bufSize, copy = next > totalRows ? totalRows - pos : bufSize;
          for (i = 0; i < cols; i++) System.arraycopy(buffer[i], 0, row[i], pos, copy);
          pos = next;
        }
        for (i = 0; i < cols; i++) col[i] = Col.getValues(types[i], row[i]);//create cols from buffer
      } catch (ArrayStoreException e) {
        throw new IllegalStateException("type of column [" + names[i] + "] should be " + types[i] +
          " ( error rowId between " + pos + " ~ " + (pos + bufSize) + " / " + totalRows + " )");
      }
    }
    full.clear();
    return new ModelCol(totalRows, col, names);
  }
}

final class BuilderQFS extends BuilderCol {
  static final int BUF_ROWS = 1000, VER = 1;
  private final String pathQFS;
  private transient RandomAccessFile raf;
  private transient BufferOutput ob;
  private transient IO[] io;
  private transient LongList pos;

  BuilderQFS(String pathQFS, String... names) {
    super(BUF_ROWS, names);
    this.pathQFS = pathQFS;
  }

  @Override
  void beforeFirstSaveBuffer() {//first time, create RandomOutput, IO and pos
    super.beforeFirstSaveBuffer();
    pos = new LongList();
    final int n = cols;
    io = new IO[n];
    for (int i = 0; i < n; i++) io[i] = IO.getInstance(types[i]);//for each col
    try {
      QUtil.createNewFile(new File(pathQFS));//create Folder, then trim file
      raf = new RandomAccessFile(pathQFS, "rw");
      ob = new BufferOutput(bufSize << 2);//x 4
      raf.seek(20);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  void saveBuffer(Object[][] buffer, int buffered) {
    try {
      for (int i = 0, n = cols; i < n; i++) {//for each col
        io[i].writeValues(ob.newOOS(), buffered, buffer[i]);//3.data
        pos.add(ob.writeTo(raf));//save pos
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  Model createModel(int totalRows) {
    final int n = cols;
    final long[] posAll;
    try {
      final long po = raf.getFilePointer();
      pos.add(po);
      final ObjectOutput oo = ob.newOOS();
      ob.writeInt(bufSize, totalRows, n);//4.buffer 5.rows 6.cols
      for (int i = 0; i < n; i++) {//for each col
        oo.writeObject(names[i]);//7.colName
        io[i].writeType(oo, types[i]);//8.colType
      }
      ob.writeLong(posAll = pos.toArray()).writeTo(raf);//9.posAll
      raf.seek(0);
      raf.writeInt(VER);//1.ver
      raf.writeLong(po);//2.skipPos
      raf.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    return new ModelQFS(pathQFS, bufSize, totalRows, names, types, io, posAll);
  }
}

final class BuilderMap<K extends Comparable, V> extends Model implements QBuilder.KeyValue<K, V> {
  private final Map<K, V> map;
  private final Class<K> keyType;
  private final Class<V> valueType;
  private transient Map.Entry<K, V>[] row;
  private transient Col[] cols;

  BuilderMap(Map<K, V> map, Class<K> keyType, Class<V> valueType) {
    super("KEY", "VALUE");
    this.map = map;
    this.keyType = keyType;
    this.valueType = valueType;
  }

  public BuilderMap<K, V> add(K key, V value) {
    map.put(key, value);
    return this;
  }

  public BuilderMap<K, V> addAll(Map<K, V> data) {
    map.putAll(data);
    return this;
  }

  public BuilderMap<K, V> remove(K key) {
    map.remove(key);
    return this;
  }

  @SuppressWarnings("unchecked")
  private Map.Entry<K, V> getEntry(int index) {
    final int n = map.size();
    if (null == row || row.length != n) row = map.entrySet().toArray(new Map.Entry[n]);
    return row[index];
  }

  Col[] getCols() {
    return null == cols ? cols = new Col[]{new CK(), new CV()} : cols;
  }

  public Object getValue(int row, int col) {
    return 0 == col ? getEntry(row).getKey() : getEntry(row).getValue();
  }

  public int getRowCount() {
    return map.size();
  }

  public QModel toModel() {
    return this;
  }

  public QModel toModel(String keyCol, String valueCol) {
    name[0] = keyCol;
    name[1] = valueCol;
    return this;
  }

  private final class CK extends Col {
    K getRow(int row) {
      return getEntry(row).getKey();
    }

    Class getType() {
      return keyType;
    }

    int expectSize() {
      return getRowCount();
    }

    @Override
    int compare(int row1, int row2) {
      return Formula.compare(getEntry(row1).getKey(), getEntry(row2).getKey());
    }
  }

  private final class CV extends Col {
    V getRow(int row) {
      return getEntry(row).getValue();
    }

    Class getType() {
      return valueType;
    }

    int expectSize() {
      return getRowCount();
    }
  }
}

final class BuilderUnion implements QBuilder.Union {
  private final String pathQFS;
  private String[] names;
  private BuilderCol builder;

  BuilderUnion(String pathQFS, String... names) {
    this.pathQFS = pathQFS;
    this.names = names;
  }

  private void _add(Model model) {
    if (QUtil.isEmpty(model)) return;
    final Col[] cols;
    if (null == builder)//first time
      builder = BuilderCol.getInstance(pathQFS, null == names ? names = model.getColNames() : names)
        .setTypes(cols = model.to().getCols(names));
    else cols = model.to().getCols(names);
    builder._addAll(cols, model.getRowCount());
  }

  public BuilderUnion add(QModel model) {
    _add((Model) model);
    return this;
  }

  public BuilderUnion add(QModel... models) {
    for (final QModel model : models) _add((Model) model);
    return this;
  }

  public int getRowCount() {
    return builder.getRowCount();
  }

  public QModel toModel() {
    return builder.toModel();
  }
}

final class BuilderPOJO<T> implements QBuilder.POJO<T> {
  private final Class<T> klass;
  private final Map<String, Converter> nameMethod = new LinkedHashMap<String, Converter>();
  private final Collection<T> rows = new ArrayList<T>();

  public BuilderPOJO(Class<T> pojoClass) {
    this.klass = pojoClass;
  }

  public BuilderPOJO<T> method(String colName, String getterMethodName) {
    nameMethod.put(colName, ConvertMethod.getInstance(klass, getterMethodName, null, null));
    return this;
  }

  public BuilderPOJO<T> method(String colName, String getterMethodName, Class[] argTypes, Object... args) {
    nameMethod.put(colName, ConvertMethod.getInstance(klass, getterMethodName, argTypes, args));
    return this;
  }

  public BuilderPOJO<T> field(String colName, String fieldName) {
    nameMethod.put(colName, ConvertField.getInstance(klass, fieldName));
    return this;
  }

  public BuilderPOJO<T> add(T value) {
    rows.add(value);
    return this;
  }

  @SuppressWarnings({"unchecked"})
  public BuilderPOJO<T> add(T... values) {
    rows.addAll(Arrays.asList(values));
    return this;
  }

  public int getRowCount() {
    return rows.size();
  }

  @SuppressWarnings({"unchecked"})
  public QModel toModel() {
    return new ModelPOJO(StringUtil.toArray(nameMethod.keySet()),
      rows.toArray(), nameMethod.values().toArray(new Converter[nameMethod.size()]));
  }
}

final class ConvertMethod implements Converter {
  private final Method method;
  private final Object[] args;

  private ConvertMethod(Method method, Object... args) {
    this.method = method;
    this.args = args;
    method.setAccessible(true);
  }

  @SuppressWarnings({"unchecked"})
  static Converter getInstance(Class klass, String methodName, Class[] argTypes, Object[] args) {
    try {
      return new ConvertMethod(klass.getMethod(methodName, argTypes), args);
    } catch (NoSuchMethodException e) {
      try {
        return new ConvertMethod(klass.getDeclaredMethod(methodName, argTypes), args);
      } catch (NoSuchMethodException e1) {
        throw new RuntimeException(e);
      }
    }
  }

  public Object to(Object value) {
    try {
      return method.invoke(value, args);
    } catch (Exception e) {
      return null;
    }
  }

  public Class toType() {
    return method.getReturnType();
  }
}

final class ConvertField implements Converter {
  private final Field field;

  private ConvertField(Field field) {
    this.field = field;
    field.setAccessible(true);
  }

  static ConvertField getInstance(Class klass, String fieldName) {
    try {
      return new ConvertField(klass.getDeclaredField(fieldName));
    } catch (NoSuchFieldException e) {
      throw new RuntimeException(e);
    }
  }

  void set(Object object, Object value) throws IllegalAccessException {
    field.set(object, value);
  }

  public Object to(Object value) {
    try {
      return field.get(value);
    } catch (Exception e) {
      return null;
    }
  }

  public Class toType() {
    return field.getType();
  }
}

abstract class Convert<F, T> implements Converter<F, T> {
  private static final Convert NUM_DBL, NUM_INT, NUM_LONG, OBJ_DUMMY;
  private static final Convert STR_DBL, STR_INT, STR_LONG, STR_DATE, STR_TEXT;
  private final Class<T> to;

  Convert(Class<T> to) {
    this.to = to;
  }

  @SuppressWarnings({"unchecked"})
  static Convert detect(Class from, Object[] sample) {
    final Detect d = String.class == from ? new Text() : BigDecimal.class == from ? new BigNum() :
      Comparable.class == from || Object.class == from || null == from ? new Any() : null;
    if (null == d) throw new IllegalArgumentException("Detect " + from + " not supported!");
    for (final Object o : sample) if (null != o && d.stop(o)) break;
    return d.getConvert();
  }

  public final Class<T> toType() {
    return to;
  }

  @SuppressWarnings({"unchecked"})
  void modify(Object[] buffer, int bufSize) {
    for (int i = 0; i < bufSize; i++) {
      final F value = (F) buffer[i];
      if (null != value) buffer[i] = to(value);
    }
  }

  static {
    NUM_DBL = new Convert<Number, Double>(Double.class) {
      public Double to(Number nonNull) {
        return nonNull.doubleValue();
      }
    };
    NUM_INT = new Convert<Number, Integer>(Integer.class) {
      public Integer to(Number nonNull) {
        return nonNull.intValue();
      }
    };
    NUM_LONG = new Convert<Number, Long>(Long.class) {
      public Long to(Number nonNull) {
        return nonNull.longValue();
      }
    };
    STR_DBL = new Convert<String, Double>(Double.class) {
      public Double to(String nonNull) {
        try {
          return "NA".equals(nonNull) ? null : Double.parseDouble(nonNull);
        } catch (RuntimeException e) {
          return null;
        }
      }
    };
    STR_INT = new Convert<String, Integer>(Integer.class) {
      public Integer to(String nonNull) {
        try {//parse 1.0 => 1 can't use parseInt
          return "NA".equals(nonNull) ? null : (int) Double.parseDouble(nonNull);
        } catch (RuntimeException e) {
          return null;
        }
      }
    };
    STR_LONG = new Convert<String, Long>(Long.class) {
      public Long to(String nonNull) {
        try {
          return "NA".equals(nonNull) ? null : Long.parseLong(nonNull);
        } catch (RuntimeException e) {
          return null;
        }
      }
    };
    STR_DATE = new Convert<String, Date>(Date.class) {
      public Date to(String nonNull) {
        return "*****".equals(nonNull) ? null : TypeUtil.toDate(nonNull);
      }
    };
    STR_TEXT = new Convert<String, String>(String.class) {
      public String to(String nonNull) {
        return "*****".equals(nonNull) ? null : nonNull;
      }
    };
    OBJ_DUMMY = new Dummy(Comparable.class);
  }

  private interface Detect<T> {
    boolean stop(T nonNull);

    Convert getConvert();
  }

  private static final class Dummy extends Convert {
    @SuppressWarnings({"unchecked"})
    Dummy(Class to) {
      super(to);
    }

    public Object to(Object nonNull) {
      return nonNull;
    }

    @Override
    void modify(Object[] buffer, int bufSize) {/*do nothing*/ }
  }

  private static final class BigNum implements Detect<BigDecimal> {
    private Stat.Range range = Stat.ofRange();

    public boolean stop(BigDecimal nonNull) {//when not long/int
      final double d = nonNull.doubleValue();
      range.add(d);
      if (d == nonNull.longValue()) return false;
      range = null;//means double
      return true;
    }

    public Convert getConvert() {
      return null == range ? NUM_DBL :
        range.max() > Integer.MAX_VALUE || range.min() < Integer.MIN_VALUE ? NUM_LONG : NUM_INT;
    }
  }

  private static final class TxtNum implements Detect<String> {
    private Stat.Range range = Stat.ofRange();

    public boolean stop(String nonNull) {
      final double d = TypeUtil.toDouble(nonNull);
      if (null == range || Double.isNaN(d)) return false;//NA
      range.add(d);
      if (d != (long) d) range = null;//means double
      return false;//never stop
    }

    public Convert getConvert() {
      return null == range ? STR_DBL :
        range.max() > Integer.MAX_VALUE || range.min() < Integer.MIN_VALUE ? STR_LONG : STR_INT;
    }
  }

  private static final class Text implements Detect<String> {
    private boolean date = true;
    private Detect num = new TxtNum();

    @SuppressWarnings({"unchecked"})
    public boolean stop(String nonNull) {//when str
      if (null != num)
        if (StringUtil.isNumber(nonNull)) num.stop(nonNull);
        else if (!"NA".equals(nonNull)) num = null;//not num
      if (date && !"*****".equals(nonNull) && null == DateUtil.parseDateTime(nonNull)) date = false;//not date
      return null == num && !date;//not num and not date
    }

    public Convert getConvert() {
      return date ? STR_DATE : null == num ? STR_TEXT : num.getConvert();
    }
  }

  private static final class Any implements Detect<Object> {
    private Class type;
    private Detect test;

    @SuppressWarnings({"unchecked"})
    public boolean stop(Object nonNull) {//when mixed
      final Class c = nonNull.getClass();
      if (null == type) test = String.class == (type = c) ? new Text() : BigDecimal.class == c ? new BigNum() : null;
      if (type == c) {//specified
        if (null != test) test.stop(nonNull);
        return false;
      }
      type = null;//mixed
      return true;
    }

    @SuppressWarnings({"unchecked"})
    public Convert getConvert() {
      return null == type ? OBJ_DUMMY : null == test ? new Dummy(type) : test.getConvert();
    }
  }
}